/*
 * mcp23s17.c
 *
 *  Created on: Nov 25, 2011
 *      Author: Doug
 *
 * Copyright (C) 2011-2012 Doug Brown
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

#include "mcp23s17.h"
#include <avr/io.h>
#include <util/delay.h>
#include <stdbool.h>

static bool MCP23S17_Inited = false;

// Pin definitions on PORTB
#define SPI_CS			(1 << 0)
#define SPI_SCK			(1 << 1)
#define SPI_MOSI		(1 << 2)
#define SPI_MISO		(1 << 3)
#define MCP23S17_RESET	(1 << 7)

#define ASSERT_CS()		PORTB &= ~SPI_CS
#define DEASSERT_CS()	PORTB |= SPI_CS;

// A few register defines
#define MCP23S17_CONTROL_WRITE(address)	(0x40 | (address << 1))
#define MCP23S17_CONTROL_READ(address)	(0x40 | (address << 1) | 1)

#define MCP23S17_IODIRA			0x00
#define MCP23S17_IODIRB			0x01
#define MCP23S17_IPOLA			0x02
#define MCP23S17_IPOLB			0x03
#define MCP23S17_GPINTENA		0x04
#define MCP23S17_GPINTENB		0x05
#define MCP23S17_DEFVALA		0x06
#define MCP23S17_DEFVALB		0x07
#define MCP23S17_INTCONA		0x08
#define MCP23S17_INTCONB		0x09
#define MCP23S17_IOCON			0x0A
#define MCP23S17_IOCON_AGAIN	0x0B
#define MCP23S17_GPPUA			0x0C
#define MCP23S17_GPPUB			0x0D
#define MCP23S17_INTFA			0x0E
#define MCP23S17_INTFB			0x0F
#define MCP23S17_INTCAPA		0x10
#define MCP23S17_INTCAPB		0x11
#define MCP23S17_GPIOA			0x12
#define MCP23S17_GPIOB			0x13
#define MCP23S17_OLATA			0x14
#define MCP23S17_OLATB			0x15

// Private functions
void MCP23S17_WriteBothRegs(uint8_t addrA, uint16_t value);
uint16_t MCP23S17_ReadBothRegs(uint8_t addrA);
uint8_t MCP23S17_ByteRW(uint8_t b);

void MCP23S17_Init()
{
	// If it has already been initialized, no need to do it again.
	if (MCP23S17_Inited)
	{
		return;
	}

	// Initialize the SPI pins
	// Set MOSI, SCLK, and CS as outputs, MISO as input
	// (Also, set the MCP23S17 reset line as an output)
	DDRB |= SPI_CS | SPI_SCK | SPI_MOSI | MCP23S17_RESET;
	DDRB &= ~SPI_MISO;

	// Initialize the SPI peripheral
	// We can run it at 8 MHz (divider of 2 from 16 MHz system clock -- maximum speed of MCP23S17 10 MHz)
#if ((F_CPU / 2) > 10000000)
#error This code assumes that the CPU clock divided by 2 is less than or equal to the MCP23S17's maximum speed of 10 MHz, and in this case, it's not.
#endif

	SPCR = 	(0 << SPIE) | // No SPI interrupts
			(1 << SPE) |  // Enable SPI
			(0 << DORD) | // MSB first
			(1 << MSTR) | // Master mode
			(0 << CPOL) | // SPI mode 0,0
			(0 << CPHA) |
			(0 << SPR0);  // SCK frequency = F_CPU/2 (because of SPI2X being set below

	SPSR = (1 << SPI2X);  // Double the SPI clock rate -- allows /2 instead of /4

	// Leave CS deasserted
	DEASSERT_CS();

	// Pull the MCP23S17 out of reset (it's pulled down to GND on the board with a 100k pulldown
	// so it won't activate during AVR ISP programming)
	PORTB |= MCP23S17_RESET;

	_delay_ms(50);

	// All done!
	MCP23S17_Inited = true;
}

void MCP23S17_SetDDR(uint16_t ddr)
{
	// The MCP23S17's DDR is backwards from the AVR's.
	// I like dealing with it so it behaves like the AVR's,
	// so I invert any DDR values in this driver.
	// In other words, when you set or get the DDR through
	// this driver, the 1s and 0s are backwards from what
	// the MCP23S17's datasheet says, but they are
	// consistent with the AVR. I value the consistency more.
	MCP23S17_WriteBothRegs(MCP23S17_IODIRA, ~ddr);
}

void MCP23S17_SetPins(uint16_t data)
{
	MCP23S17_WriteBothRegs(MCP23S17_GPIOA, data);
}

uint16_t MCP23S17_ReadPins(void)
{
	return MCP23S17_ReadBothRegs(MCP23S17_GPIOA);
}

void MCP23S17_SetPullups(uint16_t pullups)
{
	MCP23S17_WriteBothRegs(MCP23S17_GPPUA, pullups);
}

// Determines the output values of output pins without reading any input pins
uint16_t MCP23S17_GetOutputs(void)
{
	return MCP23S17_ReadBothRegs(MCP23S17_OLATA);
}

uint16_t MCP23S17_GetDDR(void)
{
	// As I mentioned above, DDR bits are inverted from
	// what the MCP23S17's datasheet says, but
	// consistent with what the AVR's datasheet says
	return ~MCP23S17_ReadBothRegs(MCP23S17_IODIRA);
}

uint16_t MCP23S17_GetPullups(void)
{
	return MCP23S17_ReadBothRegs(MCP23S17_GPPUA);
}

uint8_t MCP23S17_ByteRW(uint8_t b)
{
	SPDR = b;
	while ((SPSR & (1 << SPIF)) == 0);
	return SPDR;
}

void MCP23S17_WriteBothRegs(uint8_t addrA, uint16_t value)
{
	// addrA should contain the address of the "A" register.
	// the chip should also be in "same bank" mode.

	ASSERT_CS();

	// Start off the communication by telling the MCP23S17 that we are writing to a register
	MCP23S17_ByteRW(MCP23S17_CONTROL_WRITE(0));

	// Tell it the first register we're writing to (the "A" register)
	MCP23S17_ByteRW(addrA);

	// Write the first byte of the register
	MCP23S17_ByteRW((uint8_t)((value >> 8) & 0xFF));

	// It should auto-increment to the "B" register, now write that
	MCP23S17_ByteRW((uint8_t)(value & 0xFF));

	DEASSERT_CS();
}

uint16_t MCP23S17_ReadBothRegs(uint8_t addrA)
{
	uint16_t returnVal;

	ASSERT_CS();

	// Start off the communication by telling the MCP23S17 that we are reading from a register
	MCP23S17_ByteRW(MCP23S17_CONTROL_READ(0));

	// Tell it which register we're reading from (the "A" register)
	MCP23S17_ByteRW(addrA);

	// Read the first byte of the register
	returnVal = (((uint16_t)MCP23S17_ByteRW(0)) << 8);

	// It should auto-increment to the "B" register, now read that
	returnVal |= MCP23S17_ByteRW(0);

	DEASSERT_CS();

	return returnVal;
}
